#include "HeightMap.hpp"

HeightMap* HeightMap::mainHeightMap = NULL;

HeightMap::HeightMap() {
	//m_offset = vec3(-250.0, -200.0, -7.0);
	m_offset = vec3(0.0f, 0.0f, 0.0f);
	caseDimension = 10.0f;
	m_heightMap = NULL;
	m_normalMap = NULL;

	// registering heightmap pointer for access via python module
	mainHeightMap = this;
	m_shadowProjectionMatrix = NULL;
};

HeightMap::~HeightMap() {
	cout << "freeing height and normal array for HeightMap ... ";
	if (m_heightMap) delete m_heightMap;
	if (m_normalMap) delete m_normalMap;
	cout << " done." << endl;
};

void HeightMap::setShadowProjectionMatrix(GLfloat* mat) {
	m_shadowProjectionMatrix = mat;
};

void HeightMap::loadMap(string filename, int wX, int wY) {
	// storing map dimensions
	mapWidth = wX; mapHeight = wY;
	scaleFactor = 10.0f;

	// loading map height texture from image, storing image dimensions
	GenericImage* img = MaterialGestionner::getImage(filename);
	imageWidth = img->getWidth();
	imageHeight = img->getHeight();
	const unsigned char* tmp = img->getData();

	// creating height map array
	m_heightMap = new GLfloat[wX * wY];
	// computing height
	for (int x = 0; x < wX; x++) { 
		float tmpImgX = ((float) x) / (wX-1) * (imageWidth-1);
		int imgX = tmpImgX;

		for (int y = 0; y < wY; y++) {
			float tmpImgY = ((float) y) / (wY-1) * (imageHeight-1);
			int imgY = tmpImgY;
			// 4* because data is of format RGBA
			float height = tmp[4*(imgX * imageWidth + imgY)];
			height += tmp[4*(imgX * imageWidth + imgY)+1];
			height += tmp[4*(imgX * imageWidth + imgY)+2];
			height /= (255.0 * 3);
			m_heightMap[x * mapWidth + y] = scaleFactor * height;//- 12.5; 
		};
	};
	// we computes normals for use with lightning
	m_normalMap = new vec3[ mapWidth * mapHeight];
	for (int x = 0; x < wX; x++) 
		for (int y = 0; y < wY; y++) {
		m_normalMap[x * mapWidth + y] = normalComp(x,y);
	}
	cout << "HEIGHTMAP : begining buffers creation ...";
	createBuffer();
	cout << " -- DONE -- " << endl;
}


void HeightMap::loadMaterial(string textureName) {
	m_material = MaterialGestionner::addTexturedMaterial("mapMaterial",textureName,"shaders/map.vert","shaders/map.frag");

	// registering GLuint identifier for uniform sampler of shader
	m_grassTex_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"grassTex");
	m_sandTex_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"sandTex");
	m_seaTex_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"seaTex");
	m_tex_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"tex");
	m_shadowMapTexture_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"shadowMapTexture");
	m_shadowProjectionMatrix_id = glGetUniformLocation(m_material->getShaderProgram()->handle(),"shadowProjectionMatrix");

	// retrieving and setting material texture identifier
	GLuint grassTex = MaterialGestionner::getTexture("data/map/grass.jpg");	
	GLuint sandTex = MaterialGestionner::getTexture("data/map/sand.jpg");
	GLuint seaTex = MaterialGestionner::getTexture("data/map/sea.jpg");

	ShaderTexturedMaterial* material = (ShaderTexturedMaterial*) m_material;
	material->setTexture(grassTex, 1);
	material->setTexture(sandTex, 2);
	material->setTexture(seaTex, 3);
}

void HeightMap::createCollisionMap() {
	float seaLevel = 0.0;
	for (int x = 0; x < mapWidth - 1; x++) 
		for (int y = 0; y < mapHeight - 1; y++) {
			float h0, h1, h2, h3;
			h0 = m_heightMap[x * mapWidth + y];		
			h1 = m_heightMap[(x+1) * mapWidth + y];		
			h2 = m_heightMap[x * mapWidth + (y+1)];		
			h3 = m_heightMap[(x+1) * mapWidth + (y+1)];		

			if (h0 < seaLevel || h1 < seaLevel || h2 < seaLevel || h3 < seaLevel) {
				m_collisionMap[x * mapWidth + y] = true; // under sea level
			} else m_collisionMap[x * mapWidth + y] = false;
	};
}

bool HeightMap::getCollision(float x_, float y_) {
	float x = x_ - m_offset.x;
	float y = y_ - m_offset.y;
	int ix =  x / caseDimension;
	int iy = y / caseDimension;
	return m_collisionMap[ix * mapWidth + iy];
};

void HeightMap::recreateBuffer() {
	glDeleteBuffers(3, m_buffers);
	createBuffer();	
}

void HeightMap::createBuffer() {
	// array creation
	GLfloat* pointArray = new GLfloat[mapHeight * mapWidth * 2 * 3];
	GLfloat* normalArray = new GLfloat[mapHeight * mapWidth * 2 * 3];
	GLfloat* texCoordsArray = new GLfloat[mapWidth * mapHeight * 2 * 2]; 

	// array filling
	for (int x = 0; x < mapWidth - 1; x++) 
		for (int y = 0; y < mapHeight - 1; y++) {
			float s,t;
			s = x; s /= (mapWidth-1);  
			t = y;
			t /= (mapHeight - 1);
			// first point
			pointArray[6 * x * mapHeight + 6 * y] = x * caseDimension;
			pointArray[6 * x *mapHeight + 6 * y +1] = y * caseDimension;
			pointArray[6 * x * mapHeight + 6 * y +2] = m_heightMap[x * mapHeight + y];
			normalArray[6 * x * mapHeight + 6 * y] = m_normalMap[x * mapHeight + y].x;
			normalArray[6 * x * mapHeight + 6 * y+1] = m_normalMap[x * mapHeight + y].y;
			normalArray[6 * x * mapHeight + 6 * y+2] = m_normalMap[x * mapHeight + y].z;
			texCoordsArray[4 * x * mapHeight + 4 * y] = s;
			texCoordsArray[4 * x * mapHeight + 4 * y+1] = t;


			// second point
			s = x+1;
			s /= (mapWidth - 1);
			pointArray[6 * x * mapHeight + 6 * y +3] = (x+1) * caseDimension;
			pointArray[6 * x * mapHeight + 6 * y +4] = y * caseDimension;
			pointArray[6 * x * mapHeight + 6 * y +5] = m_heightMap[(x+1) * mapHeight + y];
			normalArray[6 * x * mapHeight + 6 * y+3] = m_normalMap[(x+1) * mapHeight + y].x;
			normalArray[6 * x * mapHeight + 6 * y+4] = m_normalMap[(x+1) * mapHeight + y].y;
			normalArray[6 * x *  mapHeight + 6 * y+5] = m_normalMap[(x+1) * mapHeight + y].z;
			texCoordsArray[4 * x * mapHeight + 4 * y+2] = s;
			texCoordsArray[4 * x * mapHeight + 4 * y+3] = t;
	};
	// array for storing the two buffers ids
	m_buffers = new GLuint[3];
	glGenBuffers(3, m_buffers);

	// point coords buffer
	glBindBuffer(GL_ARRAY_BUFFER, m_buffers[0]);
	glBufferData(GL_ARRAY_BUFFER, mapWidth * mapHeight * 6 *sizeof(GLfloat) ,pointArray, GL_STATIC_DRAW);
	// normal vector buffer
	glBindBuffer(GL_ARRAY_BUFFER, m_buffers[1]);
	glBufferData(GL_ARRAY_BUFFER, mapWidth * mapHeight * 6 * sizeof(GLfloat), normalArray, GL_STATIC_DRAW);
	// texture coords buffer
	glBindBuffer(GL_ARRAY_BUFFER, m_buffers[2]);
	glBufferData(GL_ARRAY_BUFFER, mapWidth * mapHeight * 4 * sizeof(GLfloat), texCoordsArray, GL_STATIC_DRAW);
	// binding to null buffer
	glBindBuffer(GL_ARRAY_BUFFER, 0);

	delete pointArray;
	delete normalArray;
	delete texCoordsArray;
}

void HeightMap::drawByBuffer() {
	// binding vertex buffer
	glBindBuffer(GL_ARRAY_BUFFER,m_buffers[0]);
	glVertexPointer(3,GL_FLOAT,3*sizeof(GLfloat),0); 
	glEnableClientState(GL_VERTEX_ARRAY);

	// texture point buffer
	glBindBuffer(GL_ARRAY_BUFFER,m_buffers[2]);
	glTexCoordPointer(2,GL_FLOAT,2*sizeof(GLfloat),0);
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);

	glBindBuffer(GL_ARRAY_BUFFER, m_buffers[1]);
	glNormalPointer(GL_FLOAT, 0 , 0);
	glEnableClientState(GL_NORMAL_ARRAY);

	unsigned int i = 0;
	glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
	for (i = 0; i < mapHeight - 2; i++) {
		glDrawArrays(GL_QUAD_STRIP,i * 2 * mapHeight, 2 * (mapHeight  - 2));
	}


	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	glDisableClientState(GL_VERTEX_ARRAY);
	glDisableClientState(GL_NORMAL_ARRAY);
}


void HeightMap::render() {
//#if 0
	// saving previous matrix state on stack
	glPushMatrix();
	glLoadIdentity();

	// binding material
	if (m_material) m_material->use();

	// offset move
	glTranslatef(m_offset.x, m_offset.y, m_offset.z);

	// setting texture sampler
	glUniform1i(m_tex_id, 0);
	glUniform1i(m_grassTex_id, 1);
	glUniform1i(m_sandTex_id, 2);
	glUniform1i(m_seaTex_id, 3);
	glUniform1i(m_shadowMapTexture_id,4);
	if (m_shadowProjectionMatrix) 
		glUniformMatrix4fv(m_shadowProjectionMatrix_id,1,GL_FALSE,m_shadowProjectionMatrix);

	glColor3f(1.0f, 1.0f, 1.0f);
	
	// rendering through buffers
	drawByBuffer();

	// coming back to openGL state previous to render function call
	glPopMatrix();
	glActiveTexture(GL_TEXTURE0);
	if (m_material) m_material->unuse();
//#endif
	//drawDirectly();
}

void HeightMap::drawDirectly() {
	glPushMatrix();
	glLoadIdentity();
	glTranslatef(m_offset.x, m_offset.y, m_offset.z);

	glUseProgram(0);
	m_material->use();

	// setting texture sampler
	glUniform1i(m_tex_id, 0);
	glUniform1i(m_grassTex_id, 1);
	glUniform1i(m_sandTex_id, 2);
	glUniform1i(m_seaTex_id, 3);
	glUniform1i(m_shadowMapTexture_id,4);
	//if (m_shadowProjectionMatrix) 
	//	glUniformMatrix4fv(m_shadowProjectionMatrix_id,1,GL_FALSE,m_shadowProjectionMatrix);

	glColor3f(1.0f, 0.0f, 0.0f);

	for (int x = 1; x < mapHeight-2; x++) 
		for (int y = 1; y < mapWidth-2; y++) {
			float s,t;
			s = x; s /= (mapWidth-1);  
			t = y;
			t /= (mapHeight - 1);

			glBegin(GL_TRIANGLES);
				// first triangles
				glTexCoord2f(s,t);
				setNormal(x,y);
				glVertex3f(x * caseDimension, y * caseDimension, m_heightMap[x * mapWidth + y]);
				t = y+1; t /= (mapHeight - 1);
				glTexCoord2f(s,t);
				setNormal(x,y+1);
				glVertex3f(x * caseDimension, (y+1) * caseDimension, m_heightMap[x * mapWidth + (y+1)]);
				s = x+1; s /= (mapWidth-1);  
				t = y; t /= (mapHeight - 1);
				glTexCoord2f(s,t);
				setNormal(x+1,y);
				glVertex3f((x+1) * caseDimension, y * caseDimension, m_heightMap[(x+1) * mapWidth + y]);

				// second triangle
				
				glTexCoord2f(s,t);
				setNormal(x+1,y);
				glVertex3f((x+1) * caseDimension, y * caseDimension, m_heightMap[(x+1) * mapWidth + y]);
				s = x; s /= (mapWidth-1);  
				t = y+1; t /= (mapHeight - 1);
				glTexCoord2f(s,t);
				setNormal(x,y+1);
				glVertex3f(x * caseDimension, (y+1) * caseDimension, m_heightMap[x * mapWidth + (y+1)]);
				s = x+1; s /= (mapWidth-1);  
				t = y+1; t /= (mapHeight - 1);
				glTexCoord2f(s,t);
				setNormal(x+1,y+1);
				glVertex3f((x+1) * caseDimension, (y+1) * caseDimension, m_heightMap[(x+1) * mapWidth + (y+1)]);
				
			glEnd();
	};
	m_material->unuse();
	glColor4f(1.0,1.0,1.0,1.0);
	glUseProgram(0);
	glPopMatrix();
	glActiveTexture(GL_TEXTURE0);
};



vec3 HeightMap::normalComp(int vertexIndexX, int vertexIndexY) {
	vec3 vmx, vpx, vmy, vpy, v;
	v = vec3(0,0,m_heightMap[vertexIndexX * mapWidth + vertexIndexY]);
	// left vertex
	if (vertexIndexX == 0) vmx = vec3(-1.0, 0, 0);
	else vmx = vec3(-1.0, 0, m_heightMap[(vertexIndexX - 1) * mapWidth + vertexIndexY]);
	// right vertex
	if (vertexIndexX == mapWidth - 1) vpx = vec3(1.0,0,0);
	else vpx = vec3(1.0, 0, m_heightMap[(vertexIndexX + 1) * mapWidth + vertexIndexY]);
	// down vertex
	if (vertexIndexY == 0) vmy = vec3(0.0, -1.0, 0);
	else vmx = vec3(0, -1.0, m_heightMap[(vertexIndexX) * mapWidth + (vertexIndexY - 1)]);
	// up vertex
	if (vertexIndexY == mapHeight - 1) vpy = vec3(0,1.0,0.0);
	else vpy = vec3(0, 1.0, m_heightMap[vertexIndexX * mapWidth + (vertexIndexY + 1)]);
	vec3 v1,v2,v3,v4;
	v1 = vpx - v; v1.z /= scaleFactor;
	v2 = vpy - v; v2.z /= scaleFactor;
	v3 = vmx - v; v3.z /= scaleFactor;
	v4 = vmy - v; v4.z /= scaleFactor;
	vec3 prod1 = glm::cross(v1,v2);
	vec3 prod2 = glm::cross(v3,v4);
	return  prod1 + prod2;
}

void HeightMap::changeTexture(string textureName, int id) {
	GLuint newTexture = MaterialGestionner::forceLoadingTexture(textureName);
	if (id > 3 or id < 0) {
		std::cout << "ERROR : invalid texture Level." << std::endl;
		std::cout << "        heightmap contains only 4 levels of texture 0,1,2 and 3." << std::endl;
	}
	((ShaderTexturedMaterial*) m_material)->setTexture(newTexture,id);
}

void HeightMap::setOffset(float ox, float oy, float oz) {
	m_offset = vec3(ox,oy,oz);
}

void HeightMap::setNormal(int x, int y) {
	vec3 n = m_normalMap[x * mapWidth + y];
	glNormal3f(n.x, n.y, n.z);
}

float HeightMap::getHeight(float x, float y){
	float px = x - m_offset.x;
	float py = y - m_offset.y;
	int ix, iy;// = px; int iy = py;
	ix = px / caseDimension;
	iy = py / caseDimension;
	if (ix < 0 || ix > mapHeight - 1 || iy < 0 || iy > mapWidth -1) return 0.0;
	float rx,ry;
	rx = (px - ix * caseDimension);// / caseDimension;
	ry = (py - iy * caseDimension);// / caseDimension;

	// height
	float hp00 = m_heightMap[ix * mapWidth + iy];
	float hp10 = m_heightMap[(ix + 1) * mapWidth + iy]; 
	float hp01 = m_heightMap[ix * mapWidth + (iy + 1)]; 
	float hp11 = m_heightMap[(ix +1) * mapWidth + (iy+1)]; 

	if (rx + ry <= 1.0) {
		// first triangle (x,y) (x, y+1) (x+1,y)
		vec3 v1(0.0, caseDimension, hp01 - hp00);
		vec3 v2(caseDimension, 0.0, hp10 - hp00);
		vec3 v = normalize(cross(v2, v1));
		// affine calculus
		float z = (v.x * rx + v.y * ry) / -v.z;
		return hp00 + z;
	} else {
		// second triangle (x+1,y+1) (x, y+1) (x+1,y)
		vec3 v1(0.0, -caseDimension, hp01 - hp00);
		vec3 v2(-caseDimension, 0.0, hp10 - hp00);
		vec3 v = normalize(cross(v2, v1));
		// affine calculus
		float z = (v.x * (rx - 1.0)  + v.y * (ry - 1.0)) / -v.z;
		return hp11 + z;
	};
};

void HeightMap::modifyHeight(glm::vec2 coords_, float factor_) {
	int ix = (coords_.x - m_offset.x) / caseDimension;
	int iy = (coords_.y - m_offset.y) / caseDimension;
	m_heightMap[ix * mapWidth + iy] += factor_;
	m_heightMap[(ix+1) * mapWidth + iy] += factor_;
	m_heightMap[ix * mapWidth + (iy+1)] += factor_;
	m_heightMap[(ix+1) * mapWidth + (iy+1)] += factor_;
}



void HeightMap::save() {
	// saving heightmap into "save.png" file
	char* data = new char[mapWidth * mapHeight * 4];

	// determining min and max
	float min = m_heightMap[0];
	float max = m_heightMap[0];
	for (int i = 0; i < mapWidth * mapHeight; i++) {
		float h = m_heightMap[i];
		if (min > h) min = h;
		if (max < h) max = h;
	}

	// building data array
	float amplitude =  255.0 / (max - min);
	for (int i = 0; i < mapWidth * mapHeight; i++) {
		float h = m_heightMap[i];
		unsigned int tmp = ((h - min) * amplitude);
		data[4*i] = tmp; 
		data[4*i+1] = tmp; 
		data[4*i+2] = tmp; 
		data[4*i+3] = 0xff;
	}
	// saving image
	createImage(mapWidth, mapHeight, data, "save.png");
};
